错误和异常处理

错误和异常处理

异常类型

Python 有两种错误很容易辨认:

异常处理

try/except - 异常捕获

关于这个异常对象的属性和方法,参考 内置异常 — Python 3.10.12 文档

简单实践如下:

try:
    print(f"{12 / 0}")
    print("it's ok")
# 一个 try 语句可能包含多个 except 子句,分别来处理不同的特定的异常。最多只有一个分支会被执行
except ValueError as error:
    print(f"输出 {error}")
    # 使用 raise 语句直接将捕获的异常抛出,甚至都不需要异常的变量名
    raise
except ZeroDivisionError as error:
    print("error occurred")
    print(f"输出 {error}")
# 你可以把多个异常写到一起,放到一个元组里
except (RuntimeError, TypeError, NameError) as error:
    print(f"输出 {error}")
# 可以捕获所有的异常,这个一般放在最后,这样可以用于捕获任意异常
except Exception as e:
    print(e)
# 上面的 except 也可以省略异常的类型
except:
    print("uncaught exception,just raise it")
    # 使用 raise 语句直接将捕获的异常抛出,甚至都不需要异常的变量名
    raise

输出

error occurred
输出 division by zero

else 子句

try/except 语句还有一个可选的 else 子句,else 子句,必须放在所有的 except 子句之后。如果没有 except 子句,那也就不能有 else 子句

else 子句将在 try 子句没有发生任何异常的时候执行。有异常的时候会走 except 之后直接走 finally(如果有的话)。

使用 else 子句最大的好处是让我们可以不把所有的语句放到 try 子句中(说的就是你,Java!),这样可以避免一些意想不到,而 except 又无法捕获的异常。

else 子句是 Java 中没有的

简单实践如下:

try:
    print(f"{12 / 2}")
    print("it's ok")
except ZeroDivisionError as error:
    print("error occurred")
    print(f"输出 {error}")
except:
    print("uncaught exception,just raise it")
    # 使用 raise 语句直接将捕获的异常抛出,甚至都不需要异常的变量名
    raise
else:
    print("it's alright")

输出

6.0
it's ok
it's alright

当然,你也可以在 else 子句中再次使用 try/except

finally 子句 - 清理行为

finally 子句无论异常是否发生都会执行

简单实践如下:

try:
    print(f"{12 / 0}")
    print("it's ok")
except ZeroDivisionError as error:
    print("error occurred")
    print(f"输出 {error}")
except:
    print("uncaught exception,just raise it")
    # 使用 raise 语句直接将捕获的异常抛出,甚至都不需要异常的变量名
    raise
else:
    print("it's alright")
finally:
    print("finally action : calculate over ")

输出

error occurred
输出 division by zero
finally action : calculate over 

注意,如果一个异常在 try 子句里(或者在 except 和 else 子句里)被抛出,而又没有任何的 except 把它截住,那么这个异常会在 finally 子句执行后被抛出。

这个跟 Java 其实是一样的逻辑:

异常要么被捕捉到,要么继续向上抛出,没有第三种可能

try:
    print(f"{12 / 0}")
    print("it's ok")
finally:
    print("finally action : calculate over ")

return 语句

我们假设一个场景,一个方法中,包含一个 try-except-else-finally 代码块,这个代码块中,try 子句、except 子句、else 子句、finally 子句最后都有 return 语句,同时 try-except-else-finally 后面也有方法语句,在方法的最后也有 return 语句,总共 5 个 return 语句,那调用这个方法,最终返回的,是哪个 return 的值呢?

总的来说,规律就是:

方法内部只要执行了 return 语句,都会直接跳出当前方法,后面还有 return 语句也会被忽略,但是如果当前 return 语句的后面有 finally 子句,那在执行当前 return 语句之后,并不是直接返回,而是还要去执行完 finally 子句才能返回,此时,如果 finally 代码块中的代码会影响 return 表达式的值,最终返回的结果跟执行 finally 代码块之前一样,不会改变,就好像已经固化了一样,而如果 finally 子句也有 return,那就会以 finally 子句的 renturn 做为最终结果返回,这个有点像返回值的覆盖

通过断点就看得很清楚,执行完 try 的 return 语句之后,下一步,直接跳过了 else 语句,而直接到了 finally 子句。

经过严谨测试,以上规律,在 JavaScript 和 Java 中的也是一样的,

简单实践如下:

# 通过注释 try-except-else-finally 中各个子句的 return 语句来看看最终生效的是哪个 return
def try_return():
    print("try_return")
    return 11


def test_return_in_excetp():
    try:
        print(f"{12 / 0}")
        print("it's ok")
        # return try_return()
    except ZeroDivisionError as error:
        print("error occurred")
        print(f"输出 {error}")
        return 12
    except:
        print("uncaught exception,just raise it")
        # 使用 raise 语句直接将捕获的异常抛出,甚至都不需要异常的变量名
        raise
    else:
        print("it's alright")
        return 13
    finally:
        print("finally action : calculate over ")
        return 14

    print("function run")
    return 20


print(test_return_in_excetp())

输出

error occurred
输出 division by zero
finally action : calculate over 
14

finally 不会修改 return 语句的值

print("---- finally 不会修改 return 语句的值 ----")

value = 10

def try_return():
    print("try_return")
    return value


def test_return_in_excetp():
    try:
        # print(f"{12 / 0}")
        print("it's ok")
        return try_return()
    except ZeroDivisionError as error:
        print("error occurred")
        print(f"输出 {error}")
        return 12
    except:
        print("uncaught exception,just raise it")
        # 使用 raise 语句直接将捕获的异常抛出,甚至都不需要异常的变量名
        raise
    else:
        print("it's alright")
        return 13
    finally:
        print("finally action : calculate over ")
        value = 11

        # return 14

    print("function run")
    return 20


print(test_return_in_excetp())

输出

it's ok
try_return
finally action : calculate over 
10

预定义的 finally - with 关键字

参考博客:Python with 关键字 | 菜鸟教程

大神博客:Python3: 异常处理 与 with 语句_python with 捕获异常_谢 TS 的博客-CSDN 博客

finally 代码块可以看作是一种清理行为,即,不管代码报不报错,finally 都要执行,这里就不得不提到 with 关键字

Python 中的 with 语句用于异常处理,封装了 try-finally 编码范式,提高了易用性。with 语句使代码更清晰、更具可读性,它简化了文件流等公共资源的管理。在处理文件对象时使用 with 关键字是一种很好的做法。

with 语句实现原理建立在上下文管理器(可以将其理解为一个接口)之上。上下文管理器是一个实现 __enter__ 和 __exit__ 方法的类

with obj as f:
    # f.method(...)
    pass
  1. obj 表示一个对象(或是一个表达式,结果为一个对象)

  2. 调用 obj 对象的 __enter__ 方法,返回值赋值给 as 右边的变量 f,

即:f = obj.__enter__()

  1. 执行 with 代码块中的代码

  2. 执行完 with 代码块中的代码后,无论是否发生异常,调用 obj 的 __exit__ 方法,

即:obj.__exit__(...)

上面代码相当于:

obj = ...
f = obj.__enter__()
try:
    # f.method(...)
    pass
finally:
    obj.__exit__(...)

as 也可以省略,此时 __enter__ 方法可以不返回对象

with obj:
    # obj.method(...)
    pass

文件对象实现上下文管理器

在文件对象中定义了 __enter____exit__ 方法,即文件对象也实现了上下文管理器,首先调用 __enter__ 方法,然后执行 with 语句中的代码,最后调用 __exit__ 方法。即使出现错误,也会调用 __exit__ 方法,也就是会关闭文件流。

typing.pyIO 类中可以看到这两个方法的定义:

@abstractmethod
def __enter__(self) -> 'IO[AnyStr]':
    pass

@abstractmethod
def __exit__(self, type, value, traceback) -> None:
    pass

其中 __exit__ 的实现确实时 close 方法

self.stream.close()

自定义类实现上下文管理器

当然,你在自定义类的时候,也可以实现上下文管理器,然后对这个类型的对象使用 with as 语法。

在实现了上下文管理器的自定义类中,我们一般会提供三个方法,

注意,其实这个时候,with as 后面处理的对象,不一定是自定义类的实例,虽然通常是,但是可以不是,其可以是一个别的变量。

简单实践如下:

class test_keyword_with():

    def do_work(self):
        print("doing work")
        print(12 / 0)

    def __enter__(self):
        print("enter obj")
        # 重点,需要返回当前对象,这个对象将赋值给 with as 语句中 as 后面的那个变量
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("exit obj")
        # 通过 exc_type(异常类型), (exc_val)异常信息 我们可以处理异常信息;exc_tb 异常栈对象
        # 类型比较,直接比较即可
        if exc_type is not None:
            # 可以针对特定类型进行处理,比如日志输出
            # 注意,我们没有办法在这里取消异常的抛出,也就是人们常说的吃掉这个异常
            print(exc_val)


# with test_keyword_with() as with_obj:
#     with_obj.do_work()

# as 也可以省略,此时 __enter__ 方法可以不返回对象
with_obj = test_keyword_with()
with with_obj:
    # 在这里进行了处理,__exit__ 方法中就不会有异常信息了
    try:
        with_obj.do_work()
    except ZeroDivisionError as e:
        print("exception occur",e)

输出

enter obj
doing work
exception occur division by zero
exit obj

@contextmanager 装饰器 - 相当好用

Python 中的装饰器和 Java 中的注解的异同点和讨论

浅谈 java 中注解和 python 中装饰器的区别_装饰器和注解的区别_BeanInJ 的博客-CSDN 博客

https://www.zhihu.com/question/345262158/answer/828687881

具体请看《装饰器.md

之前我们想要实现上下文管理器配合 with as 使用,得自定义一个类,但是大部分的时候,我只是想对一个已经存在的对象使用上下文管理器,有没有简单的办法呢?

@contextmanager 装饰器更适用于对已存在的对象比如第三方库中的对象添加上下文管理器,自定义类实现上下文管理器的适用于自己新建的类。

有,那就是可以使用 @contextmanager 装饰器来修饰我们获取这个对象的方法,而且在 @contextmanager 装饰器装饰的方法中,写法更加灵活

@contextmanager
def some_generator(<arguments>):
    <setup>
    try:
        yield <value>
    finally:
        <cleanup>

关于 yield 关键字,请看《迭代器.md》中的 生成器 小节

@contextmanager 装饰器已经帮我们处理好了 __enter__ 和 __exit__ 方法:

然后就可以直接在 with as 中使用这个方法,所以本质上,@contextmanager 装饰器是提供了一种更加灵活的实现上下文管理器的方式

with some_generator(<arguments>) as <variable>:
    <body>

最终的效果其实就等同于:

<setup>
try:
    <variable> = <value>
    <body>
finally:
    <cleanup>

通过 @contextmanager 装饰器,我们把获取对象和之后对这个对象的清理跟获取这个对象之后的操作完全隔开了,方便我们对代码的管理。

简单实践如下:下面演示了通过上下文管理器获取一个文件对象,然后读取文件的过程。

# @contextmanager 装饰器

from contextlib import contextmanager  # 导入上下文管理器


# @contextmanager
@contextmanager
def get_file_obj():
    # 创建一个空文件
    empty_file_name = "./emptyfile.txt"
    empty_file_obj = open(empty_file_name, "w")
    empty_file_obj.close()
    empty_file_obj = open(empty_file_name, "r")

    file_obj = None
    try:
        # 文件不存在
        file_obj = open("./test.txt", "r", encoding="utf-8")
        yield file_obj
    except Exception as error:
        # 可以捕捉到异常并阻止异常的抛出,但是此时我们仍然需要返回空的文件对象给 with as 语句
        print("文件不存在")
        yield empty_file_obj
    finally:
        empty_file_obj.close()
        # 删除临时文件
        os.remove(empty_file_name)
        if file_obj != None:
            file_obj.close()


with get_file_obj() as file_obj:
    print(file_obj.readlines())

输出

文件不存在
[]

输出异常到文件中

# 输出错误信息到文件中
import traceback

try:
    print(f"{12 / 0}")
except ZeroDivisionError as error:
    print("error occurred")
    # print_exc 不指定输出文件对象的话,默认输出到 sys.stderr 文件对象
    traceback.print_exc(file=open("./error.txt", "a"))

抛出异常 - raise

BaseException 是所有异常的公共基类,Exception 继承自 BaseException,Exception 是所有非退出异常的公共基类,其他非退出异常继承自 Exception,raise 只能抛出 BaseException 异常类型(包括其子类)或 异常类型的实例,注意,可以抛出一个 异常的类型 或 异常类型的实例

我们使用 raise 语句抛出一个指定的异常。raise 后面的类型必须是一个异常的实例或者是异常的类(也就是 Exception 的子类)

类似于 Java 的 Throw 关键字,但是 Java 可没办法直接排除一个类,Python 应该是做了额外的处理

简单实践如下:

# 手动抛出异常
b = 10
if b == 10:
    # 手动抛出 ArithmeticError 异常,自定义异常提示信息
    raise ArithmeticError(f"b 的值不对:{b}")
    # 或者直接抛出类
    # raise ZeroDivisionError
    pass

Python 内置的常见的异常类型

常见的异常有

用户自定义异常

你可以通过创建一个新的异常类来拥有自己的异常。异常类继承自 Exception 类,可以直接继承,或者间接继承,大多数的异常的名字都以 "Error" 结尾,就跟标准的异常命名一样。

简单实践如下:

# 自定义异常
class MyError(Exception):
    def __init__(self, message):
        self.message = message

    def __str__(self):
        return self.message

raise MyError("自定义错误类型")

异常的传递性

异常是具有传递性的,举个例子,main 方法调用 func01,func01 调用 func02,当函数 func02 中发生异常,并且没有捕获处理这个异常的时候,异常会传递到函数 func01, 当 func01 也没有捕获处理这个异常的时候,main 函数会捕获这个异常,  这就是异常的传递性,而当所有函数都没有捕获异常的时候,程序就会报错

因此,我们捕获异常不需要把 try-except 语句写在实际报错的那一行,写在发起函数调用的地方也是可以捕获到异常的